# ----------------------------------------------------------------------------
#   ERKbJockey.rb  Keyboard Jockey commands
# ----------------------------------------------------------------------------
# Proof of concept for Jim Foltz's idea of a multi-letter
# keyboard command processor

require "EventRelay.dll"

# ----------------------------------------------------------------------------
class ERKbJockey < EventRelay
# ----------------------------------------------------------------------------

    VK_RETURN = 13
    VK_ESC = 27
    VK_BACKSPACE = 8;
    ALT_MODIFIER_KEY = 18

    ALT_MODIFIER_MASK = 32
    CONSTRAIN_MODIFIER_KEY = 16
    CONSTRAIN_MODIFIER_MASK = 4
    COPY_MODIFIER_KEY = 17
    COPY_MODIFIER_MASK = 8

    # OnKeyUp/Down key modifiers
    KEYMOD_NONE      = 0x0000
    KEYMOD_ALT       = ALT_MODIFIER_MASK;
    KEYMOD_CONTROL   = COPY_MODIFIER_MASK;
    KEYMOD_SHIFT     = CONSTRAIN_MODIFIER_MASK;

    # seconds to wait for next character
    INTER_CHARACTER_TIMEOUT = 0.8;

    SUPPRESS = true
    ALLOW = false

    # ----------------------------------
    #    initialize
    # ----------------------------------
    def initialize
        super
        #//Logit "ERKbJockey.rb initialized"

        @kbCmdTable = Hash[]
        @kbObjTable = Hash[]
        @kbInput = ""
        @bPlayBack = false;
        @bRecording = false;

        @versionStr = ERGetVersion();
        @verMajor, @verMinor, @verFixLevel, @verDate = @versionStr.split(%r{[\. ]});
        Logit("ERKbJockey version string #{@versionStr}")
        Logit("ERKbJockey version Maj:#{@verMajor},Min:#{@verMinor},Fix:#{@verFixLevel},Date:#{@verDate}")

#//        @toolsObserver = MyToolsObserver.new
#//        Sketchup.active_model.tools.add_observer(@toolsObserver)
        #//SendKeyMacro("!WR") #// Open Ruby Console
    end
    # ----------------------------------
    #    OnKeyDown
    # ----------------------------------
    def OnKeyDown(keyIn,repeat,flags)
        key = keyIn
        # Ignore keys with ALT, shift and CTRL
        if ( 0 != (flags & (KEYMOD_ALT|KEYMOD_SHIFT|KEYMOD_CONTROL)) )
            return ALLOW;
        end
        # force letters to lower case
        if ( (key >= ?A) && (key <= ?Z) )
            key = (keyIn | 0x20)
        end
        # Debugging
        #//Logit "OnKeyDown key:#{keyIn} char:#{key.chr} repeat:#{repeat} flags:#{flags}"
        #- vk_key, keyMacro = GetKeyInfo(key, flags)
        #- Logit("KeyInfo: key:#{keyIn} vk_key:#{vk_key} keyMacro:{#{keyMacro}}")

        # Kill the inter-character timer if not in playback mode
        if ( not @bPlayBack )
            ERTimerStop();
        end

        # if active tool is any TextTool, allow all keys
        #//if( @toolsObserver.getActiveTool.include?("TextTool") )
        #//    return ALLOW;
        #//end
        # if in playback mode, allow all keys until {ENTER}
        if ( @bPlayBack )
            if ( key == VK_RETURN )
                SetPlayBack( false )
                ERTimerStop();
                return SUPPRESS # fix 1.0.01 2008/12/2
            end
            return ALLOW
        end
        # Dont parse {ENTER} when no chached input # fix 1.0.05 2008/12/8
        # A single {ENTER} can be used to turn off PlayBack mode
        # But sometimes splats SketchUp when a selection is active.
        # So this routine contains an initialization guard.
        if ( (@kbInput.length == 0) && (key == VK_RETURN) )
                                  #//(key < ?\ ) || (key > ?~) ) #space to tilde
            SetPlayBack( false );
            if ( @ERKbJockeyInInit ) then
                return SUPPRESS; end;
            return ALLOW;
        end
        # backspace
        if ( (@kbInput.length > 0) && (key == VK_BACKSPACE) )
            @kbInput = @kbInput.chop
            ShowStatusText("Command: " << @kbInput)
            return SUPPRESS
        end

        # a return/enter key ends command assembly
        if ( (@kbInput.length > 0) && (key == VK_RETURN) )
            # execute valid assembled multi-char keyboard commands
            #-if ( (@kbInput.length > 1) && (@kbObjTable[@kbInput]) )
            if ( @kbObjTable[@kbInput] )        # mod 1.0.02
                # call routine from command table. Turn on PlayBack flag
                # to protect us from possible SendKeyMacro() keys
                #--SetPlayBack( true );
                begin
                    @kbObjTable[@kbInput].send(@kbCmdTable[@kbInput])
                    ShowStatusText("Command: #{@kbInput} executed");
                rescue Exception => detail
                    ShowStatusText("Command: #{@kbInput} error");
                    #//Logit( detail.message )
                    print detail.message
                    #//Logit( detail.backtrace.join("\n") )
                    print detail.backtrace.join("\n")
                end
                ClearRecording();
                # send a timed {ENTER} to get us out of playback mode
                ERTimerStart(1);        # fix 1.0.04
                return SUPPRESS;
            end
            # Not a valid mult-char keyboard command
            if ( @kbInput.length > 1 )
                # misspelled multi-char command
                ShowStatusText("Command: #{@kbInput} unknown");
            else
                # Sketchup single char command followed by {ENTER}
                PlayRecording( @kbInput );
            end
            ClearRecording();
            return SUPPRESS;
        end #if VK_RETURN

        # ESC key input
        if ((@kbInput.length > 0) && (key == VK_ESC) )
            # discard the assembled command
            ClearRecording();
            ShowStatusText("Command: ")
            return SUPPRESS;
        end

        # Assemble command and test key against commands table
        if ( ScanCmdTable( @kbInput + key.chr ) )
            AppendRecording( key.chr );
            # start a timer so we don't wait on input forever.
            #//if ( 1 == @kbInput.length )
                ERTimerStart( INTER_CHARACTER_TIMEOUT );
            #//end
            return SUPPRESS
        end
        # Ignore Unknown multi-letter commands
        if ( (@kbInput.length+1) > 1 )
            ShowStatusText("Command: #{@kbInput<<key.chr} unknown");
            ClearRecording();
            return SUPPRESS;
        end
        # Not a char in one of our commands but may be
        # a single letter SketchUp command
        if ( @kbInput.length > 0 )
            PlayRecording( @kbInput << key.chr );
            ClearRecording();
            return SUPPRESS;
        end
        ClearRecording();
        return ALLOW;
    end #OnKeyDown
    # ----------------------------------
    #    ScanCmdTable
    # ----------------------------------
    def ScanCmdTable( cmdString )
        found = false;
        @kbCmdTable.each_key{ |key|
            if ( key[0,cmdString.length] == cmdString  )
                found = true;
                break;
            end
        }
        #//Logit("ScanCmdTable found=#{found} for #{cmdString}" )
        return found;
    end
    # ----------------------------------
    #    AppendRecording
    # ----------------------------------
    def AppendRecording( keyChar )
        #//Logit("Recording #{keyChar} at #{@kbInput.length}")
        @bRecording = true
        @kbInput <<  keyChar ;
        ShowStatusText( "Command: "<< @kbInput );
    end
    # ----------------------------------
    #    ClearRecording
    # ----------------------------------
    def ClearRecording()
        #//Logit("ClearRecording")
        @bRecording = false;
        @kbInput = "";
    end
    # ----------------------------------
    #    PlayRecording
    # ----------------------------------
    def PlayRecording(sKeyInput)
        if (sKeyInput.length > 0)
            #//Logit("PlayRecording Arg[#{sKeyInput}]");
            ShowStatusText( "Executed: " << @kbInput);
            SetPlayBack ( true );
            #Timer will cancel PlayBack if a new window eats our {ENTER}
            ERTimerStart(2);
            #{ENTER} gets us out of PlayBack mode
            SendKeyMacro(sKeyInput << "{ENTER}");
        end
    end
    # ----------------------------------
    #    SetPlayBack
    # ----------------------------------
    def SetPlayBack(state)
        @bPlayBack = state;
        #//msg = @bPlayBack ? "TRUE" : "FALSE";
        #//Logit("PlayBack [#{msg}]");
    end
    # ----------------------------------
    #    OnERTimerEvent
    # ----------------------------------
    def OnERTimerEvent()
        SetPlayBack( false );
        if ( @bRecording )
            if ( @kbInput.length > 0)
                # Enter key will end recording mode
                Logit("Timer popped with #{@kbInput}")
                SendKeyMacro("{ENTER}")
            end
        end
    end
    # ----------------------------------
    #    Menus
    # ----------------------------------
    def OnMenuSelected(menuId, menuLabel)
        # Make sure the tools observer is re-instated across file new/open/save as
        #//Logit "ERCopyMove:OnMenuSelected MenuId:#{menuId} label:#{menuLabel}"
        @toolsObserver.checkToolsObserver()
        # Just before executing a menu, always turn off PlayBack mode
        SetPlayBack( false );
        return ALLOW
    end #OnMenuSelected

    # ----------------------------------
    #    RegisterCommand
    # ----------------------------------
    def RegisterCommand( cmdIn, objIn, methodIn)
        #//Logit("RegisterCommand #{cmdIn} #{objIn} #{methodIn}");
        if ( (not cmdIn.empty?) && (objIn.respond_to?(methodIn)) )
            #//Logit("#{cmdIn} #{objIn.class} #{methodIn}")
            @kbObjTable[cmdIn] = objIn;
            @kbCmdTable[cmdIn] = methodIn;
        else
            Logit("ERKbJockey::RegisterCommand error for #{cmdIn} #{methodIn}")
            puts("ERKbJockey::RegisterCommand error for #{cmdIn} #{methodIn}")
        end
    end
    # ----------------------------------
    #    ShowStatusText
    # ----------------------------------
    def ShowStatusText( aString)
        #//Sketchup.set_status_text( aString, SB_PROMPT);
    end

    # ----------------------------------
    #    GetCommandTable
    # ----------------------------------
    def GetCommandTable()
        return @kbCmdTable;
    end
    # ----------------------------------
    #    SetInitDone()
    # ----------------------------------
    def SetInitDone(state)
        @ERKbJockeyInInit = not state;
    end
    # ----------------------------------
    #    RegisterCommand
    # ----------------------------------
    def RegisterCommand( cmdIn, objIn, methodIn)
        #//Logit("RegisterCommand #{cmdIn} #{objIn} #{methodIn}");
        if ( (not cmdIn.empty?) && (objIn.respond_to?(methodIn)) )
            #//Logit("#{cmdIn} #{objIn.class} #{methodIn}")
            @kbObjTable[cmdIn] = objIn;
            @kbCmdTable[cmdIn] = methodIn;
        else
            puts("ERKbJockey::RegisterCommand failed for #{cmdIn}");
        end
    end
    # ----------------------------------
    #    ShowCommandTable
    # ----------------------------------
    def ShowCommandTable()
        #//p @kbObjTable
        p @kbCmdTable
    end

end # class ERKbJockey
# ----------------------------------------------------------------------------
class UserClass
# ----------------------------------------------------------------------------
    # ----------------------------------
    #    UserClass initialize
    # ----------------------------------
    def initialize()
        @kbJockey = ERKbJockey.new
        @kbJockey.RegisterCommand("zo", self, :ZoomOutCommand )
        @kbJockey.RegisterCommand("ze", self, :ZoomExtentsCommand )
        @kbJockey.RegisterCommand("uc",     self, :testMethod )
        @kbJockey.RegisterCommand("config", self, :UserKbCommandConfig )
        @kbJockey.RegisterCommand("smb",    self, :ScanMenuBarCommand )
        @kbJockey.RegisterCommand("luc",    self, :LoadCommandConfigFile )

        @kbJockey.ShowCommandTable(); #//inspect
        ScanMenuBarCommand();
    end

    # ----------------------------------
    #    Logit
    # ----------------------------------
    def Logit(msg)
        @kbJockey.Logit(msg);
    end
    # ----------------------------------
    #    testMethod
    # ----------------------------------
    def testMethod
        @kbJockey.Logit("UserClass testMethod was executed")
    end
    # ----------------------------------
    #    Scan Menu Bar //internal utility
    # ----------------------------------
    def ScanMenuBarCommand()
        #//menuId = @myERKbJockey.ERGetMenuID("View/Hidden Geometry");
        #//menuId = @myERKbJockey.ERGetMenuID("Camera/Standard Views/Bottom");
        #//menuId = @myERKbJockey.ERGetMenuID("View/Edge Style/Extension");
        menuId = @kbJockey.ERGetMenuID("File"); # Scan the menu structure
        LoadCommandConfigFile();
    end
    # ----------------------------------
    #    TestKeyBinder
    # ----------------------------------
    def UserKbCommandConfig()
        count = @kbJockey.ERCmdKeyConfig();
        @kbJockey.Logit("UserKbCommandConfig count [#{count}]");
        if (count) then LoadCommandConfigFile(); end;
    end
    # ----------------------------------
    #    Load Command Config
    # ----------------------------------
    def LoadCommandConfigFile()
        begin
            fileName = "c:/Usr/Proj/RubyExt/EventRelay/Debug/EventRelay/UserKBCommands.rb"
            $".delete_if {|x| x == fileName }
            load fileName;
            userkbCmds = UserKbCommandDefinitions.new(@kbJockey);
            Logit("LoadCommandConfig #{fileName}")
            @kbJockey.ShowCommandTable(); #//inspect
        rescue Exception => detail
            msg = "Error loading: #{fileName}";
            #@myERKbJockey.ShowStatusText(msg); #<-- overwritten by command end msg
            puts msg;
            Logit( msg );
            Logit( detail.message )
            print detail.message
            Logit( detail.backtrace.join("\n") )
            print detail.backtrace.join("\n")
        end
    end
    # ----------------------------------
    #    ZoomOutCommand
    # ----------------------------------
    def ZoomOutCommand()
        Logit("ZoomOutCommand executed");
    end
    # ----------------------------------
    #    ZoomExtentsCommand
    # ----------------------------------
    def ZoomExtentsCommand()
        Logit("ZoomExtentsCommand executed");
    end


end #UserClass
# ----------------------------------------------------------------------------

    myUserObj = UserClass.new()
